// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2013 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0
package com.google.appinventor.client.wizards;
import static com.google.appinventor.client.Ode.MESSAGES;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import com.google.appinventor.client.Ode;
import com.google.appinventor.client.OdeAsyncCallback;
import com.google.appinventor.client.explorer.project.Project;
import com.google.appinventor.client.settings.user.UserSettings;
import com.google.appinventor.client.tracking.Tracking;
import com.google.appinventor.client.wizards.NewProjectWizard.NewProjectCommand;
import com.google.appinventor.client.youngandroid.TextValidators;
import com.google.appinventor.shared.properties.json.JSONUtil;
import com.google.appinventor.shared.rpc.project.UserProject;
import com.google.appinventor.shared.rpc.project.youngandroid.NewYoungAndroidProjectParameters;
import com.google.appinventor.shared.rpc.project.youngandroid.YoungAndroidProjectNode;
import com.google.appinventor.shared.settings.SettingsConstants;
import com.google.gwt.cell.client.AbstractCell;
import com.google.gwt.event.dom.client.ChangeEvent;
import com.google.gwt.event.dom.client.ChangeHandler;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.http.client.Request;
import com.google.gwt.http.client.RequestBuilder;
import com.google.gwt.http.client.RequestCallback;
import com.google.gwt.http.client.RequestException;
import com.google.gwt.http.client.Response;
import com.google.gwt.json.client.JSONArray;
import com.google.gwt.json.client.JSONObject;
import com.google.gwt.json.client.JSONParser;
import com.google.gwt.json.client.JSONString;
import com.google.gwt.json.client.JSONValue;
import com.google.gwt.resources.client.ImageResource;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.user.cellview.client.CellList;
import com.google.gwt.user.cellview.client.HasKeyboardSelectionPolicy.KeyboardSelectionPolicy;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.ListBox;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.view.client.ProvidesKey;
import com.google.gwt.view.client.SelectionChangeEvent;
import com.google.gwt.view.client.SingleSelectionModel;
/**
* Wizard for importing AI2 project from a server. A 'template' is
* a partially built project that is designed to provide 'scaffolding' for a
* lesson or tutorial. It could take various forms -- e.g., just the media that
* makes up an app, or just the components but no blocks, or just a "library" of
* certain blocks, etc.
*
* The repositories can be either 'built-in' (stored on the appengine server) or
* 'external', stored on any other Web server -- provided it is stored in the
* format described below.
*
* Built-in template repositories are hosted in appengine/war/templates/
* That entire directory will be copied during build to: appengine/build/war/templates/
*
* External templates must be placed at a reachable URL on a web server.
*
* If templates are stored on a static website. Your webserver must
* implement "Cross-origin Resource Sharing (CORS). With Apache you
* can do this by using mod_headers placing a .htaccess file in the
* templates directory that contains the one line:
*
* 'Header set Access-Control-Allow-Origin "*"'.
*
* External repositories can be added/removed by the user at runtime.
*
* To add a new repository, fill in the URL to the directory that
* contains the templates direcory, for example
* http://appinventor.cs.trincoll.edu/csp/week1/ where templates is
* contained in week1.
*
* The Wizard assumes that the templates/ repository is organized as follows and uses
* the naming conventions shown here, where 'HelloPurr' is a typical project. If a
* project is stored in /templates/Project it's .json and .asc and .aia files are
* expected to be named Project.json, Project.aia, Project.asc:
*
* /templates/HelloPurr/
* /templates/HelloPurr/HelloPurr.json -- JSON description of the project
* /templates/HelloPurr/HelloPurr.aia -- the zip archive
* /templates/HelloPurr/HelloPurr.asc -- a base64 encoded version of the aia file
* /templates/HelloPurr/screenshot.png -- optional screenshot, specified in JSON file
* /templates/HelloPurr/thumbnail.png -- optional thumbnail, specified in JSON file
* /templates/SomeOtherProject/
* ...
*
* The .json file is used to construct the description of the project in the Wizard's
* dialog:
*
* {"name": "HelloPurr", "subtitle": "A purring kitty app", "description":
"<p>This is App Inventor's version of the HelloWorld app. ...", "screenshot":
"screenshot.png", "thumbnail":"thumbnail.png" }
*
* The images are used in the templates summary that is displayed in dialog when
* the use selects a repository.
*
* The base64 encoded file is the one that the Wizard imports.
*/
public class TemplateUploadWizard extends Wizard implements NewUrlDialogCallback {
// Project archive extension
private static final String PROJECT_ARCHIVE_EXTENSION = ".zip";
private static final String PROJECT_ARCHIVE_ENCODED_EXTENSION = ".asc";
public static final String TEMPLATES_ROOT_DIRECTORY = "templates/";
public static final String URL_HOST = ""; // Default uses server as host, i.e., relative addr
public static final String EXTERNAL_JSON_FILE = "templates.json";
public static final String MIT_TEMPLATES = "Built-in Templates";
public static final int MIT_TEMPLATES_INDEX = 1;
public static final int TIMEOUT = 3000; // 3 seconds
/**
* Reference to the instantiated Wizard. Reset to null when dialog
* 'Ok' or 'Cancel' buttons are clicked.
*/
private static TemplateUploadWizard instance;
/**
* Needed to retrieve existing templates from user settings
*
*/
private static UserSettings userSettings;
/**
* The current template host Url.
*/
private String templateHostUrl = "";
public void setTemplateUrlHost(String host) {
templateHostUrl = host;
}
public String getTemplateUrlHost() {
return templateHostUrl;
}
/**
* Map of dynamic (i.e., not built-in) templates.
*/
private static Map<String, ArrayList<TemplateInfo>> templatesMap =
new HashMap<String, ArrayList<TemplateInfo>>();
/**
* Json representation of a template repository consisting of
* one or more App Inventor projects.
* Set from ProjectService.retrieveTemplateData
*/
private static String templateDataString;
/**
* Keeps track of user-created (not built-in) repositories.
*/
private static ArrayList<String> dynamicTemplateUrls = new ArrayList<String>();
/**
* Sets the dynamic template Urls from a jsonStr. This method is
* called during start up where jsonStr is retrieved from User
* settings.
*
* @param jsonStr
*/
public static void setStoredTemplateUrls(String jsonStr) {
if (jsonStr == null || jsonStr.length() == 0)
return;
JSONValue jsonVal = JSONParser.parseLenient(jsonStr);
JSONArray jsonArr = jsonVal.isArray();
for (int i = 0; i < jsonArr.size(); i++) {
JSONValue value = jsonArr.get(i);
JSONString str = value.isString();
dynamicTemplateUrls.add(str.stringValue());
}
}
/**
* Retrieves a Json string representing the Urls of dynamic
* template repositories. Called when saving UserSettings
* during shutdown or otherwise.
*
* @return a Json string of repository Urls
*/
public static String getStoredTemplateUrls() {
String[] arr = new String[dynamicTemplateUrls.size()];
for (int k = 0; k < arr.length; k++) {
arr[k] = dynamicTemplateUrls.get(k);
}
return JSONUtil.toJson(arr);
}
/**
* Returns true if hostUrl is already part of the template library.
*
*/
public static boolean hasUrl(String hostUrl) {
return templatesMap.get(hostUrl) != null;
}
/**
* A list of built-in templates -- typically the MIT repository.
*/
private static ArrayList<TemplateInfo> builtInTemplates;
/**
* Initializes the built-in template repositories.
*
* Called by ProjectService.retrieveTemplateData, which passes
* a Json string describing the template. This variable is read
* when the user selects 'Upload Template' from the Projects Toolbar
* @param json takes the form of a string:
*
* {"name": "HelloPurr", "subtitle": "A purring kitty app", "description":
"<p>This is App Inventor's version of the HelloWorld app. For more information
see the <a href='http://appinventor.mit.edu/explore/content/hellopurr.html'
target='_blank'>HelloPurr tutorial</a>.", "screenshot": "screenshot.png",
"thumbnail":"thumbnail.png" }
*/
public static void initializeBuiltInTemplates(String json) {
templateDataString = json;
builtInTemplates = getTemplates();
templatesMap.put(MIT_TEMPLATES, builtInTemplates);
}
/**
* UI Panel holding the template list.
*/
private HorizontalPanel templatePanel;
/**
* UI Listbox of template Urls.
*/
private ListBox templatesMenu;
/**
* UI Button for removing a template Url.
*/
private Button removeButton;
/**
* Remembers the last template library selected.
*/
private int lastSelectedIndex = MIT_TEMPLATES_INDEX;
/**
* Set to true when the user has selected an external template.
*/
private boolean usingExternalTemplate = false;
/**
* Set to true when the user is inputting a new Url.
*/
private boolean newUrlTestIsPending = false;
/**
* Stores the Url the user has input, which is not
* added to the list of repositories until it is validated.
*/
private String pendingUrl = "";
/**
* Stores the name of the template project selected by user.
*/
private String selectedTemplateNAME = null;
/**
* Returns a list of Template objects containing data needed
* to load a template from a zip file. Each non-null template object
* is created from its Json string
*
* @return ArrayList of TemplateInfo objects
*/
protected static ArrayList<TemplateInfo> getTemplates() {
JSONValue jsonVal = JSONParser.parseLenient(templateDataString);
JSONArray jsonArr = jsonVal.isArray();
ArrayList<TemplateInfo> templates = new ArrayList<TemplateInfo>();
for (int i = 0; i < jsonArr.size(); i++) {
JSONValue value = jsonArr.get(i);
JSONObject obj = value.isObject();
if (obj != null)
templates.add(new TemplateInfo(obj)); // Create TemplateInfo from Json
}
return templates;
}
/**
* Creates a new project upload wizard. This is invoked either from ProjectToolbar,
* when the user chooses 'Import from repo' from the toolbar menu or from the
* retrieveExternalTemplateData's callback method, if an external template library
* is selected from the drop-down menu.
*
*/
public TemplateUploadWizard() {
super(MESSAGES.templateUploadWizardCaption(), true, false); // modal, adaptive sizing
instance = this;
// Initialize the UI
this.setStylePrimaryName("ode-DialogBox");
setUpUiAndFinishCommand();
}
/**
* Callback from the InputTemplateUrlWizard. Part of NewUrlDialogCallback.
* When newUrl is received an attempt is made to retrieve template data
* from that address.
*
* @param newUrl the possibly invalid Url entered by the user.
*/
@Override
public void updateTemplateOptions(String newUrl) {
if (newUrl.length() != 0) {
newUrlTestIsPending = true;
this.pendingUrl = newUrl;
retrieveSelectedTemplates(newUrl);
}
}
/**
* Sets up the UI and calls the initFinish() method passing it the
* Command that's done when 'Ok' is clicked on the Wizard dialog.
*/
private void setUpUiAndFinishCommand() {
this.addPage(createUI(builtInTemplates));
populateTemplateDialog(builtInTemplates);
initCancelCommand(new Command() {
@Override
public void execute() {
instance = null;
}
});
// Create finish command (upload a project archive)
initFinishCommand(new Command() {
@Override
public void execute() {
String filename = selectedTemplateNAME + PROJECT_ARCHIVE_EXTENSION;
// Make sure the project name is legal and unique.
if (!TextValidators.checkNewProjectName(selectedTemplateNAME)) {
center();
return;
}
NewProjectCommand callbackCommand = new NewProjectCommand() {
@Override
public void execute(Project project) {
Ode.getInstance().openYoungAndroidProjectInDesigner(project);
}
};
createProjectFromExistingZip(selectedTemplateNAME, callbackCommand);
Tracking.trackEvent(Tracking.PROJECT_EVENT, Tracking.PROJECT_ACTION_NEW_YA, filename);
instance = null;
}
});
}
/**
* The UI consists of a vertical panel that holds a drop-down list box,
* a Horizontal panel that holds the templates list (cell list) plus
* the selected template. This is inserted in the Wizard dialog.
*
* @param templates should never be null
* @return the main panel for Wizard dialog.
*/
VerticalPanel createUI(final ArrayList<TemplateInfo> templates) {
VerticalPanel panel = new VerticalPanel();
panel.setStylePrimaryName("gwt-SimplePanel");
panel.setVerticalAlignment(VerticalPanel.ALIGN_MIDDLE);
panel.setHorizontalAlignment(VerticalPanel.ALIGN_CENTER);
templatePanel = new HorizontalPanel();
templatePanel.add(makeTemplateSelector(templates));
if (templates.size() > 0)
templatePanel.add(new TemplateWidget(templates.get(0), templateHostUrl));
templatesMenu = makeTemplatesMenu();
HorizontalPanel hPanel = new HorizontalPanel();
hPanel.add(templatesMenu);
removeButton = new Button("Remove this repository", new ClickHandler() {
@Override
public void onClick(ClickEvent arg0) {
removeCurrentlySelectedRepository();
}
});
removeButton.setVisible(false);
hPanel.add(removeButton);
panel.add(hPanel);
panel.add(templatePanel);
return panel;
}
/**
* Adds a new templates host to the list of available repositories.
*
* @param hostUrl
* @param newTemplates
*/
private static void addNewTemplateHost(String hostUrl, ArrayList<TemplateInfo> newTemplates) {
templatesMap.put(hostUrl, newTemplates);
// Display the templates dialog
if (instance == null) {
if (dynamicTemplateUrls.contains(hostUrl)) {
// Don't alert because we may be invoked via repo=... approach which
// can happen multiple times.
// Window.alert("We already have that host " + hostUrl) ;
instance = new TemplateUploadWizard();
instance.setTemplateUrlHost(hostUrl);
instance.populateTemplateDialog(newTemplates);
instance.center();
return;
}
instance = new TemplateUploadWizard();
instance.setTemplateUrlHost(hostUrl);
instance.updateTemplateOptions(hostUrl);
instance.center();
} else {
instance.populateTemplateDialog(newTemplates);
}
// Update the user settings
UserSettings settings = Ode.getUserSettings();
settings.getSettings(SettingsConstants.USER_GENERAL_SETTINGS).
changePropertyValue(SettingsConstants.USER_TEMPLATE_URLS,
TemplateUploadWizard.getStoredTemplateUrls());
settings.saveSettings(null);
}
/**
* Removes a template repository.
*/
private void removeCurrentlySelectedRepository() {
boolean ok = Window.confirm("Are you sure you want to remove this repository? " +
"Click cancel to abort.");
if (ok) {
dynamicTemplateUrls.remove(templateHostUrl);
templatesMap.remove(templateHostUrl);
templatesMenu.removeItem(lastSelectedIndex);
templatesMenu.setSelectedIndex(1);
templatesMenu.setItemSelected(1, true);
removeButton.setVisible(false);
retrieveSelectedTemplates(templatesMenu.getValue(1));
// Update the user settings
UserSettings settings = Ode.getUserSettings();
settings.getSettings(SettingsConstants.USER_GENERAL_SETTINGS).
changePropertyValue(SettingsConstants.USER_TEMPLATE_URLS,
TemplateUploadWizard.getStoredTemplateUrls());
settings.saveSettings(null);
}
}
/**
* Creates a drop down menu for selecting Template repositories.
* @return the drop down menu of repository Urls.
*/
private ListBox makeTemplatesMenu() {
final ListBox templatesMenu = new ListBox();
templatesMenu.addItem(MESSAGES.templateUploadNewUrlCaption());
templatesMenu.addItem(MIT_TEMPLATES);
for (int k = 0; k < dynamicTemplateUrls.size(); k++) { // Dynamically added Urls
templatesMenu.addItem(dynamicTemplateUrls.get(k));
}
templatesMenu.setSelectedIndex(MIT_TEMPLATES_INDEX);
templatesMenu.addChangeHandler(new ChangeHandler() {
public void onChange(ChangeEvent event) {
int selectedIndex = templatesMenu.getSelectedIndex();
if (selectedIndex == 0) {
templatesMenu.setSelectedIndex(lastSelectedIndex);
usingExternalTemplate = true; // MIT templates at index 1
removeButton.setVisible(false);
new InputTemplateUrlWizard(instance).center(); // This will do a callback
} else if (selectedIndex == 1) {
removeButton.setVisible(false); lastSelectedIndex = selectedIndex;
usingExternalTemplate = false; // MIT templates at index 1
templateHostUrl = "";
retrieveSelectedTemplates(templatesMenu.getValue(selectedIndex)); // may do callback
} else {
removeButton.setVisible(true); lastSelectedIndex = selectedIndex;
usingExternalTemplate = true; // MIT templates at index 1
templateHostUrl = templatesMenu.getValue(selectedIndex);
retrieveSelectedTemplates(templatesMenu.getValue(selectedIndex)); // may do callback
}
}
});
templatesMenu.setVisibleItemCount(1); // Turns menu into a drop-down list).
return templatesMenu;
}
/**
* Retrieves the templates associated with a particular template repository.
* Called when the user selects a template library from the drop-down box.
* If the templates are already stored in the templates map, we just
* refresh the Wizard's dialog. Otherwise we retrieve the templates
* from the external hostUrl.
*
* @param hostUrl url of an external templates host -- e.g., 'http://localhost:85/'
*/
void retrieveSelectedTemplates(String hostUrl) {
ArrayList<TemplateInfo> templates = templatesMap.get(hostUrl);
if (templates == null) {
TemplateUploadWizard.retrieveExternalTemplateData(hostUrl);
} else {
populateTemplateDialog(templates);
}
}
/**
* Loads templates into the templatePanel, which consists of a
* clickable list widget of templates and a widget that displays
* a summary of the list's current selection.
*
* @param templates
*/
void populateTemplateDialog(ArrayList<TemplateInfo> templates) {
String hostUrl = "";
// Validity check for user-entered Url.
if (this.newUrlTestIsPending) {
newUrlTestIsPending = false;
hostUrl = pendingUrl;
pendingUrl = "";
if (templates != null) {
dynamicTemplateUrls.add(hostUrl);
templatesMenu.addItem(hostUrl);
templatesMenu.setSelectedIndex(templatesMenu.getItemCount()-1); // Last item
lastSelectedIndex = templatesMenu.getSelectedIndex();
usingExternalTemplate = true;
templateHostUrl = templatesMenu.getValue(lastSelectedIndex);
} else {
return;
}
}
if (templates == null)
return;
// Display the templates for the the selected Url.
for (int k = 0; k < templatePanel.getWidgetCount(); k++) {
templatePanel.getWidget(k).removeFromParent();
}
VerticalPanel parent = (VerticalPanel) templatePanel.getParent();
templatePanel.removeFromParent();
templatePanel = new HorizontalPanel();
// Add the new templates
templatePanel.add(makeTemplateSelector(templates));
if (templates.size() > 0)
templatePanel.add(new TemplateWidget(templates.get(0), templateHostUrl));
parent.add(templatePanel);
}
/**
* Creates a new project from a Zip file and lists it in the ProjectView.
*
* @param projectName project name
* @param onSuccessCommand command to be executed after process creation
* succeeds (can be {@code null})
*/
public void createProjectFromExistingZip(final String projectName,
final NewProjectCommand onSuccessCommand) {
// Callback for updating the project explorer after the project is created on the back-end
final Ode ode = Ode.getInstance();
final OdeAsyncCallback<UserProject> callback = new OdeAsyncCallback<UserProject>(
// failure message
MESSAGES.createProjectError()) {
@Override
public void onSuccess(UserProject projectInfo) {
// Update project explorer -- i.e., display in project view
if (projectInfo == null) {
Window.alert("This template has no aia file. Creating a new project with name = " + projectName);
ode.getProjectService().newProject(
YoungAndroidProjectNode.YOUNG_ANDROID_PROJECT_TYPE,
projectName,
new NewYoungAndroidProjectParameters(projectName),
this);
return;
}
Project project = ode.getProjectManager().addProject(projectInfo);
if (onSuccessCommand != null) {
onSuccessCommand.execute(project);
}
}
};
// Use project RPC service to create the project on back end using
String pathToZip = "";
if (usingExternalTemplate) {
String zipUrl = templateHostUrl + TEMPLATES_ROOT_DIRECTORY + projectName + "/" +
projectName + PROJECT_ARCHIVE_ENCODED_EXTENSION;
RequestBuilder builder = new RequestBuilder(RequestBuilder.GET, zipUrl);
try {
Request response = builder.sendRequest(null, new RequestCallback() {
@Override
public void onError(Request request, Throwable exception) {
Window.alert("Unable to load Project Template Data");
}
@Override
public void onResponseReceived(Request request, Response response) {
ode.getProjectService().newProjectFromExternalTemplate(projectName,response.getText(),callback);
}
});
} catch (RequestException e) {
Window.alert("Error fetching project zip file template.");
}
} else {
pathToZip = TEMPLATES_ROOT_DIRECTORY + projectName + "/" + projectName +
PROJECT_ARCHIVE_EXTENSION;
ode.getProjectService().newProjectFromTemplate(projectName, pathToZip, callback);
}
}
/**
* Called from Ode when a template Url is passed as GET parameter.
* The Url could take two forms:
* 1. appinventor.cs.trincoll.edu/templates/Project/Project.asc
* This is a Base64 encoded AI project. In this case the project should be opened.
* 2. appinventor.cs.trincoll.edu/templates/ or .../templates
* This is a repository with 0 or more templates. They should be
* loaded into the client and displaye in the Templates Dialog.
* @param url the template's Url
* @param onSuccessCommand command to open the project
*/
public static void openProjectFromTemplate(String url, final NewProjectCommand onSuccessCommand) {
if(!url.startsWith("http")) {
url = "http://" + url;
}
if (url.endsWith(".asc")) {
openTemplateProject(url, onSuccessCommand);
} else {
retrieveExternalTemplateData(url);
}
}
/**
* Helper method for opening a project given its Url
* @param url A string of the form "http://... .asc
* @param onSuccessCommand
*/
private static void openTemplateProject(String url, final NewProjectCommand onSuccessCommand) {
final Ode ode = Ode.getInstance();
// This Async callback is called after the project is input and created
final OdeAsyncCallback<UserProject> callback = new OdeAsyncCallback<UserProject>(
// failure message
MESSAGES.createProjectError()) {
@Override
public void onSuccess(UserProject projectInfo) {
// This just adds the new project to the project manager, not to AppEngine
Project project = ode.getProjectManager().addProject(projectInfo);
// And this opens the project
if (onSuccessCommand != null) {
onSuccessCommand.execute(project);
}
}
};
final String projectName;
if (url.endsWith(".asc")) {
projectName = url.substring(1 + url.lastIndexOf("/"), url.lastIndexOf("."));
} else {
return;
}
// If project of the same name already exists, just open it
if (!TextValidators.checkNewProjectName(projectName)) {
Project project = ode.getProjectManager().getProject(projectName);
if (onSuccessCommand != null) {
onSuccessCommand.execute(project);
}
return; // Don't retrieve the template if the project is a duplicate
}
// Here's where we retrieve the template data
// Do a GET to retrieve data at url
RequestBuilder builder = new RequestBuilder(RequestBuilder.GET, url);
try {
Request response = builder.sendRequest(null, new RequestCallback() {
@Override
public void onError(Request request, Throwable exception) {
Window.alert("Unable to load Project Template Data");
}
// Response received from the GET
@Override
public void onResponseReceived(Request request, Response response) {
// The response.getText is the zip data used to create a new project.
// The callback opens the project
ode.getProjectService().newProjectFromExternalTemplate(projectName,response.getText(),callback);
}
});
} catch (RequestException e) {
Window.alert("Error fetching template file.");
}
}
/**
* A class to stores template details.
*
*/
public static class TemplateInfo { // implements Comparable<TemplateInfo> {
public String name;
public String subtitle;
public String description;
public String thumbStr; // thumbStr and/or screenshotStr can be ""
public String screenshotStr;
public ImageResource thumbnail;
public ImageResource screenshot;
/**
* The key provider that provides the unique ID of a template, its name.
*/
public static final ProvidesKey<TemplateInfo> KEY_PROVIDER = new ProvidesKey<TemplateInfo>() {
@Override
public Object getKey(TemplateInfo item) {
return item == null ? null : item.name;
}
};
/**
* Default constructor
*/
public TemplateInfo() {
}
public TemplateInfo(String name, String subtitle, String description, String screenshot, String thumbnail) {
this.name = name;
this.subtitle = subtitle;
this.description = description;
this.screenshotStr = screenshot;
this.thumbStr = thumbnail;
}
/**
* Builds the TemplateInfo object from JSON
* @param value
*/
public TemplateInfo(JSONObject value) {
this.name = value.get("name").toString();
this.name = this.name.substring(1, this.name.length() -1);
this.subtitle = value.get("subtitle").toString();
this.subtitle = this.subtitle.substring(1, this.subtitle.length() -1);
this.description = value.get("description").toString();
this.description = this.description.substring(1, this.description.length() -1);
this.thumbStr = value.get("thumbnail").toString();
this.thumbStr = this.thumbStr.substring(1, this.thumbStr.length() -1);
this.screenshotStr = value.get("screenshot").toString();
this.screenshotStr = this.screenshotStr.substring(1, this.screenshotStr.length() -1);
};
}
/**
* A composite widget for displaying a template.
*/
public static class TemplateWidget extends Composite {
private static Label title = new Label();
private static Label subtitle = new Label();
private static Image image = new Image();
private static HTML descriptionHtml = new HTML();
private VerticalPanel panel;
public TemplateWidget(TemplateInfo info, String hostUrl) {
setTemplate(info, hostUrl);
panel = new VerticalPanel();
panel.add(title);
panel.add(subtitle);
descriptionHtml.setHTML(info.description);
panel.add(descriptionHtml);
panel.add(image);
initWidget(panel);
setStylePrimaryName("ode-ContextMenu");
}
public static void setTemplate(TemplateInfo info, String hostUrl) {
title.setText(info.name);
subtitle.setText(info.subtitle);
descriptionHtml.setHTML(info.description);
if (! info.screenshotStr.equals("")) {
String url = hostUrl + TEMPLATES_ROOT_DIRECTORY + info.name + "/" + info.screenshotStr;
image.setUrl(url);
} else {
TemplateWidget.image.setResource(Ode.getImageBundle().appInventorLogo());
}
image.setWidth("240px");
image.setHeight("400px");
// Display the screenshot if available
if (! info.screenshotStr.equals("")) {
String url = hostUrl + TEMPLATES_ROOT_DIRECTORY + info.name + "/" + info.screenshotStr;
image.setUrl(url);
}
}
}
/**
* A Cell widget for displaying a summary of a template.
*
*/
public static class TemplateCell extends AbstractCell<TemplateInfo> {
public TemplateInfo info;
private String hostUrl;
public TemplateCell(TemplateInfo info, String hostUrl) {
this.info = info;
this.hostUrl = hostUrl;
}
@Override
public void render(Context context, TemplateInfo template, SafeHtmlBuilder sb) {
if (template == null)
return;
sb.appendHtmlConstant("<table>");
// Add the thumbnail image, if available, or a default image.
sb.appendHtmlConstant("<tr><td rowspan='3'>");
if ( !template.thumbStr.equals("") ) {
String src = hostUrl + TEMPLATES_ROOT_DIRECTORY + template.name + "/" + template.thumbStr;
sb.appendHtmlConstant("<img style='width:32px' src='" + src + "'>");
} else {
ImageResource imgResource = Ode.getImageBundle().appInventorLogo();
Image img = new Image(imgResource);
String url = img.getUrl();
sb.appendHtmlConstant("<img style='width:32px' src='" + url + "'>");
}
sb.appendHtmlConstant("</td>");
// Add the name and description.
sb.appendHtmlConstant("<td style='font-size:95%;'>");
sb.appendEscaped(template.name);
sb.appendHtmlConstant("</td></tr><tr><td>");
sb.appendEscaped(template.subtitle);
sb.appendHtmlConstant("</td></tr></table>");
}
}
/**
* Creates the scrollable list of cells each of which serves as a link to a template.
*
* @param list an ArrayList of TemplateInfo
* @return A CellList widget
*/
public CellList<TemplateInfo> makeTemplateSelector(ArrayList<TemplateInfo> list) {
TemplateCell templateCell = new TemplateCell(list.get(0), templateHostUrl);
CellList<TemplateInfo> templateCellList = new CellList<TemplateInfo>(templateCell,TemplateInfo.KEY_PROVIDER);
templateCellList.setPageSize(list.size() + 10);
templateCellList.setKeyboardSelectionPolicy(KeyboardSelectionPolicy.ENABLED);
templateCellList.setWidth("250px");
templateCellList.setHeight("400px");
templateCellList.setVisible(true);
// Add a selection model to handle user selection.
final SingleSelectionModel<TemplateInfo> selectionModel =
new SingleSelectionModel<TemplateInfo>(TemplateInfo.KEY_PROVIDER);
templateCellList.setSelectionModel(selectionModel);
selectionModel.setSelected(list.get(0), true);
final TemplateUploadWizard wizard = this;
selectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() {
public void onSelectionChange(SelectionChangeEvent event) {
TemplateInfo selected = selectionModel.getSelectedObject();
if (selected != null) {
selectedTemplateNAME = selected.name;
TemplateWidget.setTemplate(selected, wizard.getTemplateUrlHost());
}
}
});
// Set the total row count. This isn't strictly necessary, but it affects
// paging calculations, so its good habit to keep the row count up to date.
templateCellList.setRowCount(list.size(), true);
// Push the data into the widget.
templateCellList.setRowData(0, list);
return templateCellList;
}
/**
* Display the UploadTemplate dialog.
*/
@Override
public void show() {
super.show();
// Wizard size (having it resize between page changes is quite annoying)
int width = 640;
int height = 600;
this.center();
setPixelSize(width, height);
super.setPagePanelHeight(580);
}
/**
* Called from ProjectToolbar when user selects a set of external templates. It uses
* JsonP to retrieve a json file from an external server.
*
* @param hostUrl, Url of the host -- e.g., http://localhost:85/
*/
public static void retrieveExternalTemplateData(final String hostUrl) {
String url = hostUrl + TEMPLATES_ROOT_DIRECTORY + EXTERNAL_JSON_FILE;
RequestBuilder builder = new RequestBuilder(RequestBuilder.GET, url);
try {
Request response = builder.sendRequest(null, new RequestCallback() {
@Override
public void onError(Request request, Throwable exception) {
Window.alert("Unable to load Project Template Data.");
if (instance != null) {
instance.populateTemplateDialog(null);
}
}
@Override
public void onResponseReceived(Request request, Response response) {
if (response.getStatusCode() != Response.SC_OK) {
Window.alert("Unable to load Project Template Data.");
return;
}
ArrayList<TemplateInfo> externalTemplates = new ArrayList<TemplateInfo>();
JSONValue jsonVal = JSONParser.parseLenient(response.getText());
JSONArray jsonArr = jsonVal.isArray();
for(int i = 0; i < jsonArr.size(); i++) {
JSONValue entry1 = jsonArr.get(i);
JSONObject entry = entry1.isObject();
externalTemplates.add(
new TemplateInfo(entry.get("name").isString().stringValue(),
entry.get("subtitle").isString().stringValue(),
entry.get("description").isString().stringValue(),
entry.get("screenshot").isString().stringValue(),
entry.get("thumbnail").isString().stringValue()));
}
if (externalTemplates.size() == 0) {
Window.alert("Unable to retrieve templates for host = " + hostUrl + ".");
return;
}
addNewTemplateHost(hostUrl, externalTemplates);
}
});
} catch (RequestException e) {
Window.alert("Error fetching external template.");
}
}
}